راهنمای جامع درک و پیادهسازی پروتکل Iterator جاوا اسکریپت که شما را قادر میسازد تا برای مدیریت بهتر دادهها، Iteratorهای سفارشی ایجاد کنید.
ابهامزدایی از پروتکل Iterator جاوا اسکریپت و Iteratorهای سفارشی
پروتکل Iterator جاوا اسکریپت یک روش استاندارد برای پیمایش ساختارهای داده فراهم میکند. درک این پروتکل به توسعهدهندگان این امکان را میدهد که به طور کارآمد با پیمایشپذیرهای داخلی (built-in iterables) مانند آرایهها و رشتهها کار کنند و پیمایشپذیرهای سفارشی خود را متناسب با ساختارهای داده و نیازهای برنامه خاص خود ایجاد کنند. این راهنما یک بررسی جامع از پروتکل Iterator و نحوه پیادهسازی پیمایشگرهای سفارشی ارائه میدهد.
پروتکل Iterator چیست؟
پروتکل Iterator تعریف میکند که چگونه یک شیء میتواند پیمایش شود، یعنی چگونه عناصر آن به صورت متوالی قابل دسترسی هستند. این پروتکل از دو بخش تشکیل شده است: پروتکل Iterable و پروتکل Iterator.
پروتکل Iterable
یک شیء زمانی Iterable (پیمایشپذیر) در نظر گرفته میشود که متدی با کلید Symbol.iterator داشته باشد. این متد باید یک شیء مطابق با پروتکل Iterator را برگرداند.
در اصل، یک شیء پیمایشپذیر میداند که چگونه یک پیمایشگر (iterator) برای خود ایجاد کند.
پروتکل Iterator
پروتکل Iterator نحوه بازیابی مقادیر از یک دنباله را تعریف میکند. یک شیء زمانی پیمایشگر (iterator) در نظر گرفته میشود که متد next() را داشته باشد که یک شیء با دو ویژگی برمیگرداند:
value: مقدار بعدی در دنباله.done: یک مقدار بولی که نشان میدهد آیا پیمایشگر به پایان دنباله رسیده است یا خیر. اگرdoneبرابر باtrueباشد، ویژگیvalueمیتواند حذف شود.
متد next() بخش اصلی پروتکل Iterator است. هر فراخوانی next()، پیمایشگر را به جلو میبرد و مقدار بعدی در دنباله را برمیگرداند. وقتی تمام مقادیر برگردانده شدند، next() یک شیء با done برابر با true برمیگرداند.
پیمایشپذیرهای داخلی (Built-in)
جاوا اسکریپت چندین ساختار داده داخلی ارائه میدهد که به طور ذاتی پیمایشپذیر هستند. این موارد عبارتند از:
- آرایهها (Arrays)
- رشتهها (Strings)
- مپها (Maps)
- ستها (Sets)
- شیء Arguments یک تابع
- TypedArrays
این پیمایشپذیرها میتوانند مستقیماً با حلقه for...of، سینتکس spread (...) و سایر ساختارهایی که به پروتکل Iterator متکی هستند، استفاده شوند.
مثال با آرایهها:
const myArray = ["apple", "banana", "cherry"];
for (const item of myArray) {
console.log(item); // Output: apple, banana, cherry
}
مثال با رشتهها:
const myString = "Hello";
for (const char of myString) {
console.log(char); // Output: H, e, l, l, o
}
حلقه for...of
حلقه for...of یک ساختار قدرتمند برای پیمایش اشیاء پیمایشپذیر است. این حلقه به طور خودکار پیچیدگیهای پروتکل Iterator را مدیریت میکند و دسترسی به مقادیر یک دنباله را آسان میسازد.
سینتکس حلقه for...of به این صورت است:
for (const element of iterable) {
// Code to be executed for each element
}
حلقه for...of پیمایشگر را از شیء پیمایشپذیر (با استفاده از Symbol.iterator) بازیابی میکند و به طور مکرر متد next() پیمایشگر را فراخوانی میکند تا زمانی که done برابر با true شود. در هر تکرار، متغیر element به ویژگی value که توسط next() برگردانده شده است، اختصاص مییابد.
ایجاد Iteratorهای سفارشی
در حالی که جاوا اسکریپت پیمایشپذیرهای داخلی را فراهم میکند، قدرت واقعی پروتکل Iterator در توانایی آن برای تعریف پیمایشگرهای سفارشی برای ساختارهای داده خود شما نهفته است. این به شما امکان میدهد نحوه پیمایش و دسترسی به دادههای خود را کنترل کنید.
در اینجا نحوه ایجاد یک پیمایشگر سفارشی آمده است:
- یک کلاس یا شیء تعریف کنید که ساختار داده سفارشی شما را نمایندگی کند.
- متد
Symbol.iteratorرا روی کلاس یا شیء خود پیادهسازی کنید. این متد باید یک شیء پیمایشگر برگرداند. - شیء پیمایشگر باید یک متد
next()داشته باشد که یک شیء با ویژگیهایvalueوdoneبرگرداند.
مثال: ایجاد یک Iterator برای یک محدوده ساده
بیایید یک کلاس به نام Range ایجاد کنیم که یک محدوده از اعداد را نشان میدهد. ما پروتکل Iterator را پیادهسازی خواهیم کرد تا امکان پیمایش اعداد در این محدوده فراهم شود.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Capture 'this' for use inside the iterator object
return {
next() {
if (currentValue <= that.end) {
return {
value: currentValue++,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
توضیح:
- کلاس
Rangeمقادیرstartوendرا در سازنده خود دریافت میکند. - متد
Symbol.iteratorیک شیء پیمایشگر برمیگرداند. این شیء پیمایشگر حالت خود را (currentValue) و یک متدnext()دارد. - متد
next()بررسی میکند که آیاcurrentValueدر محدوده قرار دارد یا خیر. اگر چنین باشد، یک شیء با مقدار فعلی وdoneبرابر باfalseبرمیگرداند. همچنینcurrentValueرا برای تکرار بعدی افزایش میدهد. - وقتی
currentValueاز مقدارendفراتر رود، متدnext()یک شیء باdoneبرابر باtrueبرمیگرداند. - به استفاده از
that = thisتوجه کنید. از آنجا که متد `next()` در یک scope متفاوت (توسط حلقه `for...of`) فراخوانی میشود، `this` در داخل `next()` به نمونه `Range` اشاره نمیکند. برای حل این مشکل، ما مقدار `this` (نمونه `Range`) را در `that` خارج از scope متد `next()` ذخیره کرده و سپس از `that` در داخل `next()` استفاده میکنیم.
مثال: ایجاد یک Iterator برای یک لیست پیوندی (Linked List)
بیایید مثال دیگری را در نظر بگیریم: ایجاد یک پیمایشگر برای ساختار داده لیست پیوندی. لیست پیوندی دنبالهای از گرهها (nodes) است که هر گره حاوی یک مقدار و یک ارجاع (اشارهگر) به گره بعدی در لیست است. آخرین گره در لیست به null (یا undefined) ارجاع میدهد.
class LinkedListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
}
append(value) {
const newNode = new LinkedListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {
value: value,
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
}
// Example Usage:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");
for (const city of myList) {
console.log(city); // Output: London, Paris, Tokyo
}
توضیح:
- کلاس
LinkedListNodeیک گره واحد را در لیست پیوندی نشان میدهد که یکvalueو یک ارجاع (next) به گره بعدی را ذخیره میکند. - کلاس
LinkedListخود لیست پیوندی را نشان میدهد. این کلاس شامل یک ویژگیheadاست که به اولین گره در لیست اشاره میکند. متدappend()گرههای جدید را به انتهای لیست اضافه میکند. - متد
Symbol.iteratorیک شیء پیمایشگر ایجاد و برمیگرداند. این پیمایشگر گره فعلی که در حال بازدید است (current) را ردیابی میکند. - متد
next()بررسی میکند که آیا گره فعلی وجود دارد (currentتهی نیست). اگر وجود داشته باشد، مقدار را از گره فعلی بازیابی میکند، اشارهگرcurrentرا به گره بعدی منتقل میکند و یک شیء با مقدار وdone: falseبرمیگرداند. - وقتی
currentتهی شود (یعنی به انتهای لیست رسیدهایم)، متدnext()یک شیء باdone: trueبرمیگرداند.
توابع مولد (Generator Functions)
توابع مولد روشی مختصرتر و زیباتر برای ایجاد پیمایشگرها ارائه میدهند. آنها از کلمه کلیدی yield برای تولید مقادیر در صورت تقاضا استفاده میکنند.
یک تابع مولد با استفاده از سینتکس function* تعریف میشود.
مثال: ایجاد یک Iterator با استفاده از یک تابع مولد
بیایید پیمایشگر Range را با استفاده از یک تابع مولد بازنویسی کنیم:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
توضیح:
- متد
Symbol.iteratorاکنون یک تابع مولد است (به*توجه کنید). - درون تابع مولد، از یک حلقه
forبرای پیمایش محدوده اعداد استفاده میکنیم. - کلمه کلیدی
yieldاجرای تابع مولد را متوقف کرده و مقدار فعلی (i) را برمیگرداند. دفعه بعد که متدnext()پیمایشگر فراخوانی شود، اجرا از جایی که متوقف شده بود (بعد از عبارتyield) از سر گرفته میشود. - وقتی حلقه به پایان میرسد، تابع مولد به طور ضمنی
{ value: undefined, done: true }را برمیگرداند که نشاندهنده پایان پیمایش است.
توابع مولد با مدیریت خودکار متد next() و پرچم done، ایجاد پیمایشگر را ساده میکنند.
مثال: مولد دنباله فیبوناچی
یک مثال عالی دیگر از استفاده از توابع مولد، تولید دنباله فیبوناچی است:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Destructuring assignment for simultaneous update
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
توضیح:
- تابع
fibonacciSequenceیک تابع مولد است. - این تابع دو متغیر
aوbرا با دو عدد اول دنباله فیبوناچی (0 و 1) مقداردهی اولیه میکند. - حلقه
while (true)یک دنباله بینهایت ایجاد میکند. - عبارت
yield aمقدار فعلیaرا تولید میکند. - عبارت
[a, b] = [b, a + b]با استفاده از تخصیص ساختارشکن (destructuring assignment)، مقادیرaوbرا به طور همزمان به دو عدد بعدی در دنباله بهروزرسانی میکند. - عبارت
fibonacci.next().valueمقدار بعدی را از مولد بازیابی میکند. از آنجا که مولد بینهایت است، شما باید تعداد مقادیری را که از آن استخراج میکنید کنترل کنید. در این مثال، ما 10 مقدار اول را استخراج میکنیم.
مزایای استفاده از پروتکل Iterator
- استانداردسازی: پروتکل Iterator یک روش سازگار برای پیمایش ساختارهای داده مختلف فراهم میکند.
- انعطافپذیری: شما میتوانید پیمایشگرهای سفارشی متناسب با نیازهای خاص خود تعریف کنید.
- خوانایی: حلقه
for...ofکد پیمایش را خواناتر و مختصرتر میکند. - کارایی: پیمایشگرها میتوانند تنبل (lazy) باشند، به این معنی که فقط در صورت نیاز مقادیر را تولید میکنند، که میتواند عملکرد را برای مجموعههای داده بزرگ بهبود بخشد. به عنوان مثال، مولد دنباله فیبوناچی در بالا فقط مقدار بعدی را زمانی محاسبه میکند که `next()` فراخوانی شود.
- سازگاری: پیمایشگرها به طور یکپارچه با سایر ویژگیهای جاوا اسکریپت مانند سینتکس spread و ساختارشکنی (destructuring) کار میکنند.
تکنیکهای پیشرفته Iterator
ترکیب Iteratorها
شما میتوانید چندین پیمایشگر را در یک پیمایشگر واحد ترکیب کنید. این کار زمانی مفید است که نیاز به پردازش دادهها از چندین منبع به صورت یکپارچه دارید.
function* combineIterators(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";
const combined = combineIterators(array1, array2, string1);
for (const value of combined) {
console.log(value); // Output: 1, 2, 3, a, b, c, X, Y, Z
}
در این مثال، تابع `combineIterators` هر تعداد پیمایشپذیر را به عنوان آرگومان میپذیرد. این تابع بر روی هر پیمایشپذیر تکرار کرده و هر آیتم را yield میکند. نتیجه یک پیمایشگر واحد است که تمام مقادیر از تمام پیمایشپذیرهای ورودی را تولید میکند.
فیلتر کردن و تبدیل Iteratorها
شما همچنین میتوانید پیمایشگرهایی ایجاد کنید که مقادیر تولید شده توسط یک پیمایشگر دیگر را فیلتر یا تبدیل میکنند. این به شما امکان میدهد دادهها را در یک خط لوله (pipeline) پردازش کنید و عملیات مختلفی را بر روی هر مقدار در حین تولید آن اعمال کنید.
function* filterIterator(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapIterator(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);
for (const value of squaredEvenNumbers) {
console.log(value); // Output: 4, 16, 36
}
در اینجا، `filterIterator` یک پیمایشپذیر و یک تابع предиکت (predicate) میگیرد. این تابع فقط آیتمهایی را که предиکت برای آنها `true` برمیگرداند، yield میکند. `mapIterator` یک پیمایشپذیر و یک تابع تبدیل میگیرد. این تابع نتیجه اعمال تابع تبدیل بر روی هر آیتم را yield میکند.
کاربردهای دنیای واقعی
پروتکل Iterator به طور گسترده در کتابخانهها و فریمورکهای جاوا اسکریپت استفاده میشود و در انواع برنامههای کاربردی دنیای واقعی، به ویژه هنگام کار با مجموعههای داده بزرگ یا عملیات ناهمزمان، ارزشمند است.
- پردازش داده: پیمایشگرها برای پردازش کارآمد مجموعههای داده بزرگ مفید هستند، زیرا به شما امکان میدهند با دادهها به صورت تکهتکه کار کنید بدون اینکه کل مجموعه داده را در حافظه بارگذاری کنید. تصور کنید در حال تجزیه یک فایل CSV بزرگ حاوی دادههای مشتری هستید. یک پیمایشگر میتواند به شما امکان دهد هر سطر را بدون بارگذاری کل فایل در حافظه به یکباره پردازش کنید.
- عملیات ناهمزمان: پیمایشگرها میتوانند برای مدیریت عملیات ناهمزمان، مانند واکشی داده از یک API، استفاده شوند. شما میتوانید از توابع مولد برای متوقف کردن اجرا تا زمان در دسترس قرار گرفتن داده و سپس از سرگیری با مقدار بعدی استفاده کنید.
- ساختارهای داده سفارشی: پیمایشگرها برای ایجاد ساختارهای داده سفارشی با الزامات پیمایش خاص، ضروری هستند. یک ساختار داده درختی را در نظر بگیرید. شما میتوانید یک پیمایشگر سفارشی برای پیمایش درخت به ترتیب خاص (مثلاً اول-عمق یا اول-سطح) پیادهسازی کنید.
- توسعه بازی: در توسعه بازی، پیمایشگرها میتوانند برای مدیریت اشیاء بازی، افکتهای ذرهای و سایر عناصر پویا استفاده شوند.
- کتابخانههای رابط کاربری: بسیاری از کتابخانههای UI از پیمایشگرها برای بهروزرسانی و رندر کارآمد کامپوننتها بر اساس تغییرات دادههای زیربنایی استفاده میکنند.
بهترین شیوهها (Best Practices)
Symbol.iteratorرا به درستی پیادهسازی کنید: اطمینان حاصل کنید که متدSymbol.iteratorشما یک شیء پیمایشگر مطابق با پروتکل Iterator برمیگرداند.- پرچم
doneرا با دقت مدیریت کنید: پرچمdoneبرای نشان دادن پایان پیمایش حیاتی است. مطمئن شوید که آن را به درستی در متدnext()خود تنظیم میکنید. - استفاده از توابع مولد را در نظر بگیرید: توابع مولد روشی مختصرتر و خواناتر برای ایجاد پیمایشگرها ارائه میدهند.
- از عوارض جانبی در
next()خودداری کنید: متدnext()باید عمدتاً بر روی بازیابی مقدار بعدی و بهروزرسانی حالت پیمایشگر تمرکز کند. از انجام عملیات پیچیده یا ایجاد عوارض جانبی در داخلnext()خودداری کنید. - پیمایشگرهای خود را به طور کامل آزمایش کنید: پیمایشگرهای سفارشی خود را با مجموعههای داده و سناریوهای مختلف آزمایش کنید تا از رفتار صحیح آنها اطمینان حاصل کنید.
نتیجهگیری
پروتکل Iterator جاوا اسکریپت یک روش قدرتمند و انعطافپذیر برای پیمایش ساختارهای داده فراهم میکند. با درک پروتکلهای Iterable و Iterator و با استفاده از توابع مولد، میتوانید پیمایشگرهای سفارشی متناسب با نیازهای خاص خود ایجاد کنید. این به شما امکان میدهد تا به طور کارآمد با دادهها کار کنید، خوانایی کد را بهبود بخشید و عملکرد برنامههای خود را افزایش دهید. تسلط بر پیمایشگرها درک عمیقتری از قابلیتهای جاوا اسکریپت را باز میکند و شما را قادر میسازد کدی زیباتر و کارآمدتر بنویسید.